 package suncertify.db;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.TreeMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
import suncertify.db.DbExceptions.DuplicateKeyException;
import suncertify.db.DbExceptions.RecordNotFoundException;

/**
 *
 * @author jshah
 * Implements the interface DB, directly accesses the low level dbfile
 */
public class Data implements DB   {

    /**
     * location of the Records Map csv file which contains record numbers and there corresponding locations
     */
    private static  String RECORDSMAP_PATH;
    //recordmap constants

    /**
     *A file which holds the mapping of a Record to location map for each Database file with a unique path
     */
    private static final String mapFileAddressFile = "\\mapFileAddresses.map";
    /**
     * The key length of a record which is 4 bytes Integer
     */
    private final int RECORDKEYLENGTH = 4;
    /**
     * The location of the record, 8bytes long value
     */
    private final int RECORDLOCATIONLENGTH = 8;
    /**
     * value to mark the deleted record in the persistent recordmaps file
     */
    private final long DELETEDRECORDLOCATION = -1L; 

    //Exception codes used, instead of throwing an exception this error codes are returned in the body of catch for those
    //exceptions
    /**
     * error code if there is an error reading the DBFile life
     */
    public static final byte DBREADERROR = -1;
    /**
     * error code if the requested record is not found in the dbfile
     */
    public static final byte RECNOTFOUND = -2;
    /**
     * error code if a <code>SecurityException</code> is thrown while accessing the DBFil
     */
    public static final byte SECURITYERROR = -3;
    /**
     * If a record is already booked
     */
    public static final byte INVALIDBOOKING = -4;

    /**
     * ASCII symbol for a white-space is 32
     */
    private final byte BLANKSPACE = 32;    //ASCII VALUE FOR A BLANK SPACE CHARACTERS

     TreeMap fieldlist;

    /**
     * FILEPTR           location where the File starts
     */
    long FILEPTR = 0;
    /**
     *  RECORDPTR         location where the record starts in the file
     */
    long RECORDPTR = 0;
    /**
     * DATAPTR           location where all the metadata ends and the data of Records begin
     */
    long DATAPTR = 0;
    /**
     * Pointer to point to start of each Field in a record
     */
    long FIELDPTR = 0;
    /**
     * FILESIZE          the size of the dbfie
     */
    long FILESIZE = 0;
    /**
     * EOF               location where Dbfile ends
     */
    long EOF = 0;
    /**
     * dbfile  the <code>RandomAccess</code> file object containing the database file
    */
     private RandomAccessFile dbfile;
    /**
     * recordsMap        the File containing Record Indexes mapped to there corresponding physical locations in the RandomAccessFile
     */
    private RandomAccessFile recordsMap;

    /**
     * mapFileAddressHash A hash which holds the Mapping for database file name to its corresponding map file
     */
    private RandomAccessFile mapFileAddressHash;
    /**
     * magicCookie       magic cookie value which identifies this as a data file
     */
    private int magicCookie;
    /**
     * lengthOfRecord    lengthOfRecord total overall length in bytes of each record
     */
    private int lengthOfRecord;
    /**
     * noOfFields        number of fields in each record
     */
    private short noOfFields;
    /**
     * totalRecords      The total number of records in the dbfile
     */
    private int totalRecords;

    /**
     * value '1'of a field in DELFLAG, denoting a Record is deleted,
     *
     */
    private final int DELFLAG = 1;
    /**
     * value '0' of a field in UNDELETEDFLAG, denoting a Record is undeleted
     */
    private final int UNDELETEFLAG = 0;

    /**
     * <code>Enum</code> containing each field of the record as a value and contains methods for there name and size
     */
    private Fields recordFields;
    /**
     * deleteQueue       Collection of locations of all the deleted records in the DBFile
     */
    private Queue<Long> deleteQueue = new LinkedList<Long>();
    /**
     *  RecordtoLocationMap Collection of locations of all the Records in the dbfile
     */
    private TreeMap<Integer, Long> RecordtoLocationMap = new TreeMap<Integer, Long>();

    /**
     * Map containing the location of mapfiles for corresponding dbfiles
     */
    private TreeMap<String, RandomAccessFile> DbToMapSet = new TreeMap<String, RandomAccessFile>();

    private Logger logger = Logger.getLogger(Data.class.getName());
    /**
     * lockRegister          LockRegister containing lock objects corresponding to all the locks in the dbfile
     */
    private LockRegister lockRegister;
    /**
     * lockManager           LockManager which manages the locking and unlocking of all the Records
     */
    private LockManager lockManager;


    private String filename;

    
    
    enum Fields {

        NAME, LOCATION, SPECIALITIES, SIZE, RATE, OWNER;
        private int size;
        private String fieldVal;

        public String getFieldVal() {
            return fieldVal;
        }

        public void setFieldVal(String searchVal) {
            this.fieldVal = searchVal;
        }

        public void setFieldSize(int fieldsize) {
            this.size = fieldsize;
        }

        public int getFieldSize() {
            return this.size;
        }
    }



    /**
     *
     * @param filename The path to the db file
     * @param mode     rw(read write) or r(read only) mode for the dbfile
     * @throws IOException <code>java.lang.IOException</code> Exception if the file not found or do not have permission for accessing the file
     */
    public Data(String filename, String mode) throws IOException {

        this.filename = filename;
        this.dbfile = new RandomAccessFile(filename, mode);
        this.FILESIZE = dbfile.length();
        this.setHeader();
        this.setSchema();
//        this.setLastRecord(lastRecord);
        this.populateDeleteQueue();
        this.InitRecordtoLocationMap();
        this.initLockRegister(this.getTotalRecords());
        this.lockManager = new LockManager(this.lockRegister);
    }


    /**
     * The size of the Map mapping RecordNo's to there Random Access Location
     * @return  the Number of records entered in the db so far also includes deleted record
     */
    public int getTotalRecords() { 

        this.totalRecords = this.RecordtoLocationMap.size();
        return totalRecords;
    }

    /**
     * Initializes a <code>TreeMap</code> to have a sorted map of record numbers to there corresponding location in the
     * <code>RandomAccess</code> DB file, The TreeMap reads a persistent <code>RandomAccess</code> recordmap.txt file to populate the
     * <code>TreeMap</code>
     * @throws IOException      throws IOException if failed to create or read RecordLocationMap file
     */
    private void InitRecordtoLocationMap() throws IOException{

        
            File curDir = new File(".");
            System.out.println(curDir.getCanonicalPath());
            RECORDSMAP_PATH = "\\" + this.filename.hashCode() + "_dbmap.map";
           
            String rfile = curDir.getCanonicalPath() + RECORDSMAP_PATH;
            File temp  =  new File(rfile);
            if (!temp.exists()) {
                if (!temp.createNewFile()) {
                    throw new IOException();
                }
                
                  this.recordsMap = new RandomAccessFile(temp,"rw");
                  for (Integer i = 0; i < this.getInitialNumberOfRecords(); i++) {
                        Integer key = i;
                        Long location = this.DATAPTR + key * (this.lengthOfRecord + 1);
                        this.recordsMap.writeInt(key);
                        if(this.deleteQueue.contains(location))
                            location = this.DELETEDRECORDLOCATION;
                        this.recordsMap.writeLong(location);

                

                }
            }
            else {
                this.recordsMap = new RandomAccessFile(temp,"rw");
            }
       
        //each entry in recordsMap is 12bytes create Name value pairs read by
        
            long filePtr = 0;
            while (filePtr < this.recordsMap.length()) {

                this.recordsMap.seek(filePtr);
                int key = (this.recordsMap.readInt());
                logger.log(Level.INFO, "key:{0}", key);
                filePtr += this.RECORDKEYLENGTH;
                this.recordsMap.seek(filePtr);
                Long location = this.recordsMap.readLong();
                logger.log(Level.INFO, "location:{0}", location);
                filePtr += this.RECORDLOCATIONLENGTH;
                if (location < 0) {
                    location = null;
                }
                this.RecordtoLocationMap.put(key, location);
               

            }
       
    }

    /**
     * Reads the DB file to get the Records count based on file size and length of each record
     * @return The total number of Records in the DB file in the initial state, it is called when the Server is initialized
     */
    private int getInitialNumberOfRecords() {
        //no# of bytes actually occupied by the record is lengthOfRecord+1
        return (int) (this.FILESIZE - this.DATAPTR) / (this.lengthOfRecord + 1);

    }

    /**
     * Get the <code>TreeMap</code> RecordtoLocationMap created from the <code>recordMap</code>
     * @return Returns a <code>TreeMap</code> created from the csv file, where Key:is record no. and Value: is the location of the record in <code>DBFile</code>
     */
    public TreeMap<Integer, Long> getRecordtoLocationMap() {
        return this.RecordtoLocationMap;
    }

    /**
     * Initializes an instance of <code>LockRegister</code> with <code>Lock</code>
     * @param NoOfRecords       The total number of records in the RecordtoLocation Map To create a <code>TreeMap</code> which contains
     * a <code>Lock</code> <code>Object</code>
     */
    private void initLockRegister(int NoOfRecords) {
        this.lockRegister = LockRegister.LockRegisterGetNewInstance(NoOfRecords);
    }

//    private void setLastRecord(int lastRecord) {
//        this.lastRecord = lastRecord;
//    }
//    private int lastRecord ;
    /**
     * Reads the RandomAccessFile and stores the metadata before the actual data part of the file starts
     * @param magicCookie       magic cookie value which identifies this as a data file
     * @param lengthOfRecord    lengthOfRecord total overall length in bytes of each record
     * @param noOfFields        number of fields in each record
     * @throws IOException      throws IOException if failed to read dbfile
     */
    private void setHeader() throws IOException{
       
            this.magicCookie = dbfile.readInt();
            System.out.println( "MAGICCOOKIE" + this.magicCookie);
            this.lengthOfRecord = dbfile.readInt();
            this.noOfFields = dbfile.readShort();
       
    }

    /**
     * Reads the RandomAccess File <code>dbfile</code> to read fields size in bytes of each field(column)
     * or a Record and sets the corresponding item in <code>Enum Fields</code> which models a Record in the file
     * @throws IOException  throws IOException if failed to read dbfile
     */
    private void setSchema() throws IOException{
            this.FILEPTR = dbfile.getFilePointer();
            short fieldname_length;
            short field_length;
            byte[] buf_fieldname;

            for (Fields f : Fields.values()) {
                //find the size of fieldname, create appropriate buffer read the fieldname, conver to string and print
                fieldname_length = dbfile.readShort();
                buf_fieldname = new byte[fieldname_length];
                dbfile.readFully(buf_fieldname);

                //find the size of field, create appropriate buffer read the field, conver to string and print
                field_length = dbfile.readShort();
                f.setFieldSize(field_length);
            }

            //DATAPTR set here
            this.DATAPTR = dbfile.getFilePointer();

        
    }

    /**
     * Reads the RandomAccess File <code>dbfile</file> and adds the location of each Record that has the delete flag set to '1'
     * the entries in <code>TreeMap deleteQueue</code> can be reused to insert a new record.
     */
    private void populateDeleteQueue() {
        int recNo = 0;
        long startptr = 0;
        while (startptr < this.FILESIZE) {
            startptr = this.DATAPTR + (recNo * (this.lengthOfRecord + 1));
            try {
                dbfile.seek(startptr);
                byte del = dbfile.readByte();
                if (del == 1) {
                    this.deleteQueue.add(startptr);
                }

            } catch (java.io.EOFException ex) {
                break;
            } catch (IOException ex) {
                logger.log(Level.SEVERE, null, ex);
            }

            recNo++;
        }
    }

    /**
     *Reads a record from the file. Returns an array where each
     *element is a record value.
     * @param recNo The logical index of Record in the RecordtoLocationMap
     * @return      A String Array where each element is a record field value
     * @throws suncertify.db.DbExceptions.RecordNotFoundException   */
    public String[] read(int recNo) throws RecordNotFoundException {

        String result = this.getRecord(recNo);
        String[] resultArray = result.split(""
                + "#");
        return resultArray;
    }

    /**
     * Gets the location of the Record by looking up the <code>recNo</code> in the <code>TreeMap RecordtoLocationMap</code>
     * Reads each field and appends the value to A String
     * @param recNo         The logical index of Record in the RecordtoLocationMap
     * @return              Returns a String composed of field values of a record separated by a marker '#'
     * @throws suncertify.db.DbExceptions.RecordNotFoundException
     */
    public String getRecord(int recNo) throws RecordNotFoundException {

        String record = "";
        record = record + recNo + "#";
        if (!RecordtoLocationMap.containsKey(recNo) || (this.RecordtoLocationMap.get(recNo) == null)|| this.deleteQueue.contains(recNo)) {
            record = null;
            logger.log(Level.WARNING,"RECORD#{0} Might Be Deleted" + ":" + "Not found", recNo);
            throw new DbExceptions().new RecordNotFoundException();
            
        }
        if (record == null) {
            return null;
        }
        FILEPTR = this.RecordtoLocationMap.get(recNo);
       
        try {
            dbfile.seek(FILEPTR);
        } catch (IOException e) {
            logger.log(Level.SEVERE, "ERROR READING RECORD",e);
        }

        //1 byte to read the delflag
        int bufSize = 1;
        try {

            int delflag = (dbfile.readByte());

            if (delflag == this.DELFLAG) {

                //The record should already be in the delete queue when it got deleted no need to add it again
          //      this.deleteQueue.add(FILEPTR);
                throw new DbExceptions().new RecordNotFoundException();
            }

        } catch (IOException ex) {
            Logger.getLogger(Data.class.getName()).log(Level.SEVERE, "ERROR ACCESSING DB FILE", ex);
        }
        for (Fields f : Fields.values()) {
            try {
                switch (f) {
                    case NAME: {
                        bufSize = Fields.NAME.getFieldSize();
                        break;
                    }
                    case LOCATION: {
                        bufSize = Fields.LOCATION.getFieldSize();
                        break;
                    }
                    case SPECIALITIES: {
                        bufSize = Fields.SPECIALITIES.getFieldSize();
                        break;
                    }
                    case SIZE: {
                        bufSize = Fields.SIZE.getFieldSize();
                        break;
                    }
                    case RATE: {
                        bufSize = Fields.RATE.getFieldSize();
                        break;
                    }
                    case OWNER: {
                        bufSize = Fields.OWNER.getFieldSize();
                        break;
                    }
                }
                byte[] fieldBuf = new byte[bufSize];


                dbfile.readFully(fieldBuf);

                record = record + new String(fieldBuf) + "#";
            } catch (IOException ex) {
               
                Logger.getLogger(Data.class.getName()).log(Level.SEVERE, "ERROR ACCESSING DB FILE", ex);
            }
        }

        return record;
    }

   /**
    * Returns an array of record numbers that match the specified
    criteria. Field n in the database file is described by
    criteria[n]. A null value in criteria[n] matches any field
    value. A non-null  value in criteria[n] matches any field
    value that begins with criteria[n]. (For example, "Fred"
    matches "Fred" or "Freddy".)
    *@param criteria   A <code>String[]</code> where each element contains the desired value for that field to be matched
    *                   eg.criteria[0] - contains the value to be matched for Name field of Record
    *                   eg.criteria[1] - contains the value to be matched for Location field of Record
    *                   eg.if criteria[0] is null it will ignore and return record numbers whose 'location' field for 
    *                   the record
    *                   eg.if criteria[0] and criteria[1] it will ignore both and match all the records and return all the record no's.
    *@return            Returns and <code>int[]</code> which contains the Record no's of all the Records that contain the field that matches
    *                   the value in <code>String[] criteria</code>
    */
    public int[] find(String[] criteria) {
        Integer recNo = 0;
        int nameIndex = 1;
        long startptr = 0;
        List<Integer> found = new ArrayList<Integer>();
        //get iterator for recordlocationmap which is basically all the record numbers which are still valid
        Iterator<Integer> i = this.RecordtoLocationMap.keySet().iterator();
        while (i.hasNext()) {
            try {
                //i.next() gives the record number use that to get the location
                recNo = i.next();             
                if (this.RecordtoLocationMap.get(recNo) == null) {
                    continue;
                }
                
                startptr = (this.RecordtoLocationMap.get(recNo));
                this.dbfile.seek(startptr);

                //read the delflag for each record
                int delflag = this.dbfile.readByte();
                if (delflag == this.DELFLAG) {
                    logger.log(Level.INFO, "deleteflag + df{0}", delflag);
                    continue;
                }
                long currptr = startptr + nameIndex;

                //read the name for each record which is a 32byte field in the DB
                byte[] temp = new byte[Fields.NAME.getFieldSize()];
                
                this.dbfile.seek(currptr);
                this.dbfile.readFully(temp);
                String curr = new String(temp);
                logger.log(Level.INFO,curr);

                //if we read a null string that means end of file so exit
                if (curr == null) {
                    break;
                }

                //if the criteria for name is null then record qualifies, if not null then
                //compare to the String which is read
                if (((criteria[0] == null) || curr.toLowerCase().startsWith(criteria[0].toLowerCase()))) {

                    //if criteria[1] i.e. Location is null then the record qualifies, otherwise
                    // read the string starting after 32bytes of name and compare it with the query
                    if (criteria[1] == null) {
                        found.add(recNo);
                    } else {
                        //Read the location for each record which is a 64byte field in the DB
                        byte[] tempLoc = new byte[Fields.LOCATION.getFieldSize()];
                        currptr = currptr + (long) Fields.NAME.getFieldSize();
                        dbfile.readFully(tempLoc);
                        String location = new String(tempLoc);
                        logger.log(Level.INFO,location);
                        if (location.toLowerCase().startsWith(criteria[1].toLowerCase())) {
                            found.add(recNo);
                        }
                    }
                }

            } catch (IOException ex) {
                logger.log(Level.SEVERE, "IOEXCEPTION WHILE TRYING TO FIND");
                break;
            }


        }

        //copy the ArrayList to array because of the signature of method
        int[] foundArray = new int[found.size()];
        for (int item = 0; item < foundArray.length; item++) {
            foundArray[item] = found.get(item);
        }
        if (foundArray.length == 0) {
            logger.log(Level.INFO,"YOUR SEARCH DID NOT MATCH ANY RECORDS");
        }
        return foundArray;
    }

    /**
     *Deletes a record, making the record number and associated disk
     *storage available for reuse.
     *Throws SecurityException if the record is locked with a cookie
     *other than lockCookie.
     * @param recNo         record number of the record in DBFile
     * @param lockCookie    a <code>long</code> code returned when the record was locked
     * @throws suncertify.db.DbExceptions.RecordNotFoundException
     * @throws SecurityException
     */
    public void delete(int recNo,  long lockCookie)
            throws RecordNotFoundException, SecurityException {
        
            //points to the delflag field of the record
            Long delflagptr = this.RecordtoLocationMap.get(recNo);
            if ((delflagptr == null) || this.deleteQueue.contains(delflagptr)) {
                logger.log(Level.WARNING, "ALREADY DELETED NO NEED TO DELETE");
                return;
            }
            try {
                dbfile.seek(delflagptr);
                dbfile.writeByte(this.DELFLAG);

                this.deleteQueue.add(delflagptr);
                //update the recordtolocation map by marking the location of this recordno as null which implies deleted record
                this.RecordtoLocationMap.put(recNo, null);

                //seek to the record# in maps file, each record is of total 12bytes (4bytes of integer key + 8 bytes of long record
                //location
                this.recordsMap.seek(recNo * (this.RECORDKEYLENGTH + this.RECORDLOCATIONLENGTH));
                //now you are pointing to the record which is just deleted, to mark it deleted write "-1L" as the location of that record
                this.recordsMap.seek(recNo * (this.RECORDKEYLENGTH + this.RECORDLOCATIONLENGTH) + this.RECORDKEYLENGTH);
                this.recordsMap.writeLong(this.DELETEDRECORDLOCATION);

                this.recordsMap.seek(recNo * (this.RECORDKEYLENGTH + this.RECORDLOCATIONLENGTH));
                logger.log(Level.INFO, "RECORD TO BE DELETED RECORDNO:{0}", this.recordsMap.readInt());
                this.recordsMap.seek(recNo * (this.RECORDKEYLENGTH + this.RECORDLOCATIONLENGTH) + this.RECORDKEYLENGTH);
                logger.log(Level.INFO, "LOCATION AFTER DELETING:{0}", this.recordsMap.readLong());

            } catch (IOException ex) {
                Logger.getLogger(Data.class.getName()).log(Level.SEVERE, "IOERROR WHILE DELETING A RECORD", ex);
            }
            //once the record is deleted unlock the record, needs to be unlocked regardless
            finally{
                this.lockManager.unlock(recNo, lockCookie);
            }
   }
    

    /**
     *Creates a new record in the database (possibly reusing a
     *deleted entry). Inserts the given data, and returns the record
     *number of the new record.
     * @param data <code>String[]</code> where each element contains a  Record field Value
     * @return     Record Index where the new entry is inserted
     * @throws suncertify.db.DbExceptions.DuplicateKeyException
     */
    public int create(String[] data) throws DuplicateKeyException {
        try {
            //check if contractor already exists
            String[] criteria = {data[0], data[1]};
            int[] found = this.find(criteria);
            if (found.length != 0) {
                for (int f : found) {
                    try {
                        String[] matchingRecord = this.read(f);
                        System.out.println(matchingRecord[1] + "&&" + data[0]);
                        if (data[0].trim().equals(matchingRecord[1].trim()) && data[1].trim().matches(matchingRecord[2].trim())) {
                            throw new DbExceptions().new DuplicateKeyException();
                        }
                    } catch (RecordNotFoundException ex) {
                        Logger.getLogger(Data.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
            long currptr = 0;
            if (this.deleteQueue.isEmpty()) {
                try {
                    currptr = dbfile.length();
                } catch (IOException ex) {
                    Logger.getLogger(Data.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                //This is always a Long for sure so safe to cast
                currptr = (Long) this.deleteQueue.poll();
            }

            int key = this.RecordtoLocationMap.lastKey() + 1;
            this.RecordtoLocationMap.put(key, currptr);
            //update the recordsMap file
            this.recordsMap.seek(this.recordsMap.length());
            this.recordsMap.writeInt(key);
            this.recordsMap.writeLong(currptr);
            dbfile.seek(currptr);
            //the cursor is now at place of deleted record, ready to overwrite that record
            //first write '0' to undelete then copy the values from Data array
            dbfile.writeByte(this.UNDELETEFLAG);
            Fields[] f = Fields.values();
            for (int i = 0; i < f.length; i++) {
                int lengthOfField = f[i].getFieldSize();
                byte[] tempBuffer = new byte[lengthOfField];
                tempBuffer = Arrays.copyOf(data[i].getBytes(), f[i].getFieldSize());
                for (int j = data[i].getBytes().length; j < tempBuffer.length; j++) {
                    tempBuffer[j] = this.BLANKSPACE; // pad with spaces instead of zero's
                }
                dbfile.write(tempBuffer, 0, tempBuffer.length);
            }
            this.FILESIZE = dbfile.length();
            this.lockRegister.addLockToRegister(key);
            return key;
        } catch (IOException ex) {
            Logger.getLogger(Data.class.getName()).log(Level.SEVERE, null, ex);
            //need a constant
            return Data.DBREADERROR;
        }
    }

    /**
     * updates the recNo by writing the customerID to the "customer" field of a record
     * @param recNo         Record index of the record to be booked
     * @param customerID    The customerID for which CSR books the Record
     * @return              Returns a flag to signal success or type of error
     *                      <code>SECURITYERROR</code> is returned if some other CSR Client which did not have the lock tried to acces the record
     *                      <code>RECORDNOTFOUND</code> is returned if the record to be booked is not found in the dbfile
     */
    public byte book(int recNo, String customerID) {

        int length = Fields.values().length;

        String[] data = new String[length];
        data[data.length - 1] = customerID;
        try {
            long longCookie = this.lockManager.lock(recNo);
            this.update(recNo, data, longCookie);
        } catch (SecurityException se) {
            logger.log(Level.WARNING,"INCORRECT KEYCODE");
            return this.SECURITYERROR;
        } catch (RecordNotFoundException re) {
            logger.log(Level.WARNING,"RECORD NOT FOUND TO BOOK");
            return this.RECNOTFOUND;
        }

        //return 0 for success
        return 0;
    }

    /**
     * Updates a Record by writing a empty string to the "Customer" field of the record
     * @param recNo Record index for record to be unlocked
     * @return      Returns a flag to signal success or type of error
     */
    public byte unbook(int recNo) {

        String[] record = null;
        String emptyString = "";
        try {
            record = this.read(recNo);
        } catch (RecordNotFoundException r) {
            logger.log(Level.WARNING,"RECORD NOT FOUND, TO UNBOOK");
            return this.RECNOTFOUND;
        }
        if (record[record.length - 1].isEmpty() || record[record.length - 1].equals(" ")) {
            logger.log(Level.WARNING,"NOT BOOKED, NOTHING TO DO");
            return this.INVALIDBOOKING;
        }
        return this.book(recNo, emptyString);
    }

    /**
     * Returns True if the record at recNo  is already booked
     * @param       recNo
     * @return      Record index
     * @throws suncertify.db.DbExceptions.RecordNotFoundException
     */
    private boolean isBooked(int recNo) throws RecordNotFoundException {

        String[] data = this.read(recNo);
        if (data[data.length - 1].length() > 0) {
            return true;
        }
        return false;
    }

    /**
     *Modifies the fields of a record. The new value for field n
     *appears in data[n]. Throws SecurityException
     *if the record is locked with a cookie other than lockCookie.
     * @param recNo         Record Index of the Record to be Updated
     * @param data          <code>String[] data</code> each element of the array is the new value of the record field,
     *                      if an element is null the corresponding field retains the current value
     * @param lockCookie    A lockCookie returned after the record is locked which is required to update the record
     * @throws suncertify.db.DbExceptions.RecordNotFoundException
     * @throws SecurityException
     */
    public void update(int recNo, String[] data, long lockCookie)
            throws RecordNotFoundException, SecurityException {

        //check if it is booked
        long currptr = this.RecordtoLocationMap.get(recNo);
        try {
            currptr = currptr + 1;
            dbfile.seek(currptr);
        } catch (IOException ex) {
            Logger.getLogger(Data.class.getName()).log(Level.SEVERE, null, ex);
        }

        Fields[] f = Fields.values();
        for (int i = 0; i < f.length; i++) {
            try {
                String toWrite = data[i];
                if (toWrite == null) {

                    currptr = dbfile.getFilePointer();
                    dbfile.seek(currptr + f[i].getFieldSize());
                    continue;

                }
                byte[] tmpBuffer = new byte[f[i].getFieldSize()];
                tmpBuffer = Arrays.copyOf(data[i].getBytes(), f[i].getFieldSize());
                for (int j = data[i].getBytes().length; j < f[i].getFieldSize(); j++) {
                    tmpBuffer[j] = this.BLANKSPACE;
                }
                dbfile.write(tmpBuffer);
            } catch (IOException ex) {
                Logger.getLogger(Data.class.getName()).log(Level.SEVERE, null, ex);
            }
            //unlock regardless
            finally{
                this.lockManager.unlock(recNo, lockCookie);
            }

        }


    }

  

    /**
     * Locks a record so that it can only be updated or deleted by this client.
     * Returned value is a cookie that must be used when the record is unlocked,
     * updated, or deleted. If the specified record is already locked by a different
     * client, the current thread gives up the CPU and consumes no CPU cycles until
     * the record is unlocked.
     * @param recNo      Record Index for which lock is desired
     * @return           returns a <code>long keycode</code> which is required to unlock the record
     * @throws SecurityException
     */
    public long lock(int recNo) throws SecurityException {

        return this.lockManager.lock(recNo);

    }

    /**
     * Releases the lock on a record. Cookie must be the cookie
     * returned when the record was locked; otherwise throws SecurityException.
     * @param recNo     Record Index for a record index which has to be unlocked
     * @param cookie    required to unlock, should match the value returned while locking that record.  If there is a mismatch
     *                  securityException is thrown
     * @throws suncertify.db.DbExceptions.RecordNotFoundException
     * @throws SecurityException
     */
    public void unlock(int recNo, long cookie) throws RecordNotFoundException, SecurityException {
        this.lockManager.unlock(recNo, cookie);
    }
}
